查看原文
其他

Spring全家桶系列--SpringBoot之AOP详解

cuifuan Java知音 2019-06-23

//本文作者:cuifuan

//本文将收录到菜单栏:《Spring全家桶》专栏中

面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。


OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。

准备工作

首先,使用AOP要在build.gradle中加入依赖

  1. //引入AOP依赖

  2. compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"


然后在application.yml中加入

  1. spring:

  2.  aop:

  3.    proxy-target-class: true

1.@Pointcut 切入点

定义一个切点。


例如我们要在一个方法加上切入点,根据方法的返回的对象,方法名,修饰词来写成一个表达式或者是具体的名字

我们现在来定义一个切点

  1. package com.example.aop;

  2. import org.aspectj.lang.annotation.Aspect;

  3. import org.aspectj.lang.annotation.Pointcut;

  4. import org.slf4j.Logger;

  5. import org.slf4j.LoggerFactory;

  6. import org.springframework.stereotype.Component;

  7. /**

  8. * 类定义为切面类

  9. */

  10. @Aspect

  11. @Component

  12. public class AopTestController {

  13.    private static final Logger logger = LoggerFactory.getLogger(AopTestController.class);

  14.    /**

  15.     * 定义一个切点

  16.     */

  17.    @Pointcut(value = "execution(public String test (..))")

  18.    public void cutOffPoint() {

  19.    }

  20. }

这里的切点定义的方法是

  1.    @GetMapping("hello")

  2.    public String test(){

  3.        logger.info("欢迎关注Java知音");

  4.        return "i love java";

  5.    }

如果你想写个切入点在所有返回对象为Area的方法,如下
@Pointcut("execution(public com.example.entity.Area (..))")
等很多写法,也可以直接作用在某些包下
注意:private修饰的无法拦截

2.@Before前置通知

在切入点开始处切入内容

在之前的AopTestController类中加入对test方法的前置通知

  1.    @Before("cutOffPoint()")

  2.    public void beforeTest(){

  3.        logger.info("我在test方法之前执行");

  4.    }

这里@Before里的值就是切入点所注解的方法名

在方法左侧出现的图标跟过去以后就是所要通知的方法 这里就是配置正确了,我们来浏览器调用一下方法

联想一下,这样的效果可以用在哪里,想像如果要扩展一些代码,在不需要动源代码的基础之上就可以进行拓展,美滋滋

3.@After 后置通知

和前置通知相反,在切入点之后执行

  1.    @After("cutOffPoint()")

  2.    public void doAfter(){

  3.        logger.info("我是在test之后执行的");

  4.    }

控制台执行结果

这里定义一个通知需要重启启动类,而修改通知方法的内容是可以热部署的

4.@Around环绕通知

和前两个写法不同,实现的效果包含了前置和后置通知。

当使用环绕通知时,proceed方法必须调用,否则拦截到的方法就不会再执行了
环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的

  1.    ThreadLocal<Long> startTime = new ThreadLocal<>();

  2.    @Around("cutOffPoint()")

  3.    public Object doAround(ProceedingJoinPoint pjp){

  4.        startTime.set(System.currentTimeMillis());

  5.        logger.info("我是环绕通知执行");

  6.        Object obj;

  7.        try{

  8.            obj = pjp.proceed();

  9.            logger.info("执行返回值 : " + obj);

  10.            logger.info(pjp.getSignature().getName()+"方法执行耗时: " + (System.currentTimeMillis() - startTime.get()));

  11.        } catch (Throwable throwable) {

  12.            obj=throwable.toString();

  13.        }

  14.        return obj;

  15.    }

执行结果:

1.环绕通知可以项目做全局异常处理
2.日志记录
3.用来做数据全局缓存
4.全局的事物处理 等

5.@AfterReturning

切入点返回结果之后执行,也就是都前置后置环绕都执行完了,这个就执行了

  1.    /**

  2.     * 执行完请求可以做的

  3.     * @param result

  4.     * @throws Throwable

  5.     */

  6.    @AfterReturning(returning = "result", pointcut = "cutOffPoint()")

  7.    public void doAfterReturning(Object result) throws Throwable {

  8.        logger.info("大家好,我是@AfterReturning,他们都秀完了,该我上场了");

  9.    }

执行结果

应用场景可以用来在订单支付完成之后就行二次的结果验证,重要参数的二次校验,防止在方法执行中的时候参数被修改等等

6.@AfterThrowing

这个是在切入执行报错的时候执行的

  1.    // 声明错误e时指定的抛错类型法必会抛出指定类型的异常

  2.    // 此处将e的类型声明为Throwable,对抛出的异常不加限制

  3.    @AfterThrowing(throwing = "e",pointcut = "cutOffPoint()")

  4.    public void doAfterReturning(Throwable e) {

  5.        logger.info("大家好,我是@AfterThrowing,他们犯的错误,我来背锅");

  6.        logger.info("错误信息"+e.getMessage());

  7.    }

在其他切入内容中随意整个错误出来,制造一个环境。

下面是@AfterThrowing的执行结果

7.AOP用在全局异常处理

定义切入点拦截ResultBean或者PageResultBean

  1.    @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")

  2.    public void handlerPageResultBeanMethod() {

  3.    }

  4.    @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")

  5.    public void handlerResultBeanMethod() {

  6.    }

下面是AopController.java

  1. package com.example.aop;

  2. import com.example.beans.PageResultBean;

  3. import com.example.beans.ResultBean;

  4. import com.example.entity.UnloginException;

  5. import com.example.exception.CheckException;

  6. import org.aspectj.lang.ProceedingJoinPoint;

  7. import org.aspectj.lang.annotation.Around;

  8. import org.aspectj.lang.annotation.Aspect;

  9. import org.aspectj.lang.annotation.Pointcut;

  10. import org.slf4j.Logger;

  11. import org.slf4j.LoggerFactory;

  12. import org.springframework.stereotype.Component;

  13. /**

  14. * 使用@Aspect注解将此类定义为切面类

  15. * 根据晓风轻著的ControllerAOP所修改

  16. * 晓风轻大佬(很大的佬哥了):https://xwjie.github.io/

  17. */

  18. @Aspect

  19. @Component

  20. public class AopController {

  21.    private static final Logger logger = LoggerFactory.getLogger(AopController.class);

  22.    ThreadLocal<ResultBean> resultBeanThreadLocal = new ThreadLocal<>();

  23.    ThreadLocal<PageResultBean<?>> pageResultBeanThreadLocal = new ThreadLocal<>();

  24.    ThreadLocal<Long> start = new ThreadLocal<>();

  25.    /**

  26.     * 定义一个切点

  27.     */

  28.    @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")

  29.    public void handlerPageResultBeanMethod() {

  30.    }

  31.    @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")

  32.    public void handlerResultBeanMethod() {

  33.    }

  34.    @Around("handlerPageResultBeanMethod()")

  35.    public Object handlerPageResultBeanMethod(ProceedingJoinPoint pjp) {

  36.        start.set(System.currentTimeMillis());

  37.        try {

  38.            pageResultBeanThreadLocal.set((PageResultBean<?>)pjp.proceed());

  39.            logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));

  40.        } catch (Throwable e) {

  41.            ResultBean<?> resultBean = handlerException(pjp , e);

  42.            pageResultBeanThreadLocal.set(new PageResultBean<>().setMsg(resultBean.getMsg()).setCode(resultBean.getCode()));

  43.        }

  44.        return pageResultBeanThreadLocal.get();

  45.    }

  46.    @Around("handlerResultBeanMethod()")

  47.    public Object handlerResultBeanMethod(ProceedingJoinPoint pjp) {

  48.        start.set(System.currentTimeMillis());

  49.        try {

  50.            resultBeanThreadLocal.set((ResultBean<?>)pjp.proceed());

  51.            logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));

  52.        } catch (Throwable e) {

  53.            resultBeanThreadLocal.set(handlerException(pjp , e));

  54.        }

  55.        return resultBeanThreadLocal.get();

  56.    }

  57.    /**

  58.     * 封装异常信息,注意区分已知异常(自己抛出的)和未知异常

  59.     */

  60.    private ResultBean<?> handlerException(ProceedingJoinPoint pjp, Throwable e) {

  61.        ResultBean<?> result = new PageResultBean();

  62.        logger.error(pjp.getSignature() + " error ", e);

  63.        // 已知异常

  64.        if (e instanceof CheckException) {

  65.            result.setMsg(e.getLocalizedMessage());

  66.            result.setCode(ResultBean.FAIL);

  67.        } else if (e instanceof UnloginException) {

  68.            result.setMsg("Unlogin");

  69.            result.setCode(ResultBean.NO_LOGIN);

  70.        } else {

  71.            result.setMsg(e.toString());

  72.            result.setCode(ResultBean.FAIL);

  73.        }

  74.        return result;

  75.    }

  76. }

用上面的环绕通知可以对所有返回ResultBean或者PageResultBean的方法进行切入,这样子就不用在业务层去捕捉错误了,只需要去打印自己的info日志。

看下面一段代码

  1.    @Transactional

  2.    @Override

  3.    public int insertSelective(Area record) {

  4.        record.setAddress("test");

  5.        record.setPostalcode(88888);

  6.        record.setType(3);

  7.        int i=0;

  8.        try {

  9.            i = areaMapper.insertSelective(record);

  10.        }catch (Exception e){

  11.            logger.error("AreaServiceImpl insertSelective error:"+e.getMessage());

  12.        }

  13.        return i;

  14.    }

假如上面的插入操作失败出错了? 你认为会回滚吗?

答案是:不会。

为什么?

因为你把错误捕捉了,事物没检测到异常就不会回滚。

那么怎么才能回滚呢?

在catch里加throw new RuntimeException().

可是那么多业务方法每个设计修改的操作都加,代码繁琐,怎么进行处理呢?

在这里用到上面的AOP切入处理,错误不用管,直接抛,抛到控制层进行处理,这样的话,接口调用的时候,出错了,接口不会什么都不返回,而是会返回给你错误代码,以及错误信息,便于开发人员查错。

8.以上用的是log4j2的日志处理

先移除springboot自带的log日志处理

在build.gradle中增加

  1. configurations {

  2.    providedRuntime

  3.    // 去除SpringBoot自带的日志

  4.    all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'

  5. }

  6. ext {

  7.    springBootVersion = '2.0.1.RELEASE'

  8. }

  9. dependencies {

  10.    compile "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}"

  11. }

然后在application.yml中增加

  1. #显示mysql执行日志

  2. logging:

  3.  level:

  4.    com:

  5.      example:

  6.        dao: debug

  7.  config: classpath:log4j2-spring.xml

log4j2-spring.xml

  1. <?xml version="1.0" encoding="UTF-8"?>

  2. <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->

  3. <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->

  4. <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->

  5. <configuration status="INFO" monitorInterval="30">

  6.    <!--先定义所有的appender-->

  7.    <appenders>

  8.        <!--这个输出控制台的配置-->

  9.        <console name="Console" target="SYSTEM_OUT">

  10.            <!--输出日志的格式-->

  11.            <PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/>

  12.        </console>

  13.        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用-->

  14.        <File name="Test" fileName="logs/test.log" append="false">

  15.            <PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/>

  16.        </File>

  17.        <RollingFile name="RollingFileInfo" fileName="logs/log.log" filePattern="logs/info.log.%d{yyyy-MM-dd}">

  18.            <!-- 只接受level=INFO以上的日志 -->

  19.            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>

  20.            <PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/>

  21.            <Policies>

  22.                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>

  23.                <SizeBasedTriggeringPolicy/>

  24.            </Policies>

  25.        </RollingFile>

  26.        <RollingFile name="RollingFileError" fileName="logs/error.log" filePattern="logs/error.log.%d{yyyy-MM-dd}">

  27.            <!-- 只接受level=WARN以上的日志 -->

  28.            <Filters>

  29.                <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" />

  30.            </Filters>

  31.            <PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/>

  32.            <Policies>

  33.                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>

  34.                <SizeBasedTriggeringPolicy/>

  35.            </Policies>

  36.        </RollingFile>

  37.    </appenders>

  38.    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->

  39.    <loggers>

  40.        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->

  41.        <logger name="org.springframework" level="INFO"></logger>

  42.        <logger name="org.mybatis" level="INFO"></logger>

  43.        <root level="all">

  44.            <appender-ref ref="Console"/>

  45.            <appender-ref ref="Test"/>

  46.            <appender-ref ref="RollingFileInfo"/>

  47.            <appender-ref ref="RollingFileError"/>

  48.        </root>

  49.    </loggers>

  50. </configuration>

之后在你要打印日志的类中增加

  1. private static final Logger logger = LoggerFactory.getLogger(你的类名.class);

  2.    public static void main(String[] args) {

  3.        logger.error("error级别日志");

  4.        logger.warn("warning级别日志");

  5.        logger.info("info级别日志");

  6.    }


有了日志后就很方便了,在你的方法接收对象时打印下,然后执行了逻辑之后打印下, 出错之后很明确了,就会很少去Debug的,养成多打日志的好习惯,多打印一点info级别的日志,用来在开发环境使用,在上线的时候把打印的最低级别设置为warning,这样你的info级别日志也不会影响到项目的重要Bug的打印

写这个博客的时候我也在同时跑着这个项目,有时候会出现一些错误,例如jar包版本,业务层引用无效,AOP设置不生效等等,也同时在排查解决,如果你遇到了同样的错误,可以去我的GitHub联系我,如小弟有时间或许也能帮到你,谢谢


Github地址:https://github.com/cuifuan


点击图片加入Spring交流群

↓↓↓

看完本文有收获?请转发分享给更多人


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存